SPB Git

spb/autoscholar

Public
25.4 KB · 666 lines tsx
1import { bibtexFor, getPaperBySlug, plainCitationFor, risFor } from "@autoscholar/core";2import { parseBib } from "@autoscholar/qa";3import { publicArtifactKey } from "@autoscholar/storage";4import type { Metadata } from "next";5import Link from "next/link";6import { notFound } from "next/navigation";7import { CopyButton } from "@/components/CopyButton";8import { Disclosure } from "@/components/Disclosure";9import { PaperCard } from "@/components/PaperCard";10import { PdfViewer } from "@/components/PdfViewer";11import { StatusBadge } from "@/components/StatusBadge";12import { Tabs } from "@/components/Tabs";13import { disciplineLabel, fmtBytes, fmtDate, modeLabel, shortHash } from "@/lib/format";14import { t } from "@/lib/i18n";15import { db, storage } from "@/lib/server/db";1617export const dynamic = "force-dynamic";1819async function load(slug: string) {20  try {21    return await getPaperBySlug(db(), slug);22  } catch {23    return null;24  }25}2627export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {28  const { slug } = await params;29  const p = await load(slug);30  if (!p) return { title: "Paper not found" };31  const base = process.env.APP_BASE_URL ?? "http://localhost:8470";32  return {33    title: p.version.title,34    description: p.version.abstract.slice(0, 300),35    alternates: { canonical: `/research/${p.paper.slug}` },36    robots: p.paper.synthetic ? { index: false, follow: false } : undefined,37    openGraph: {38      title: p.version.title,39      description: p.version.abstract.slice(0, 300),40      type: "article",41      url: `${base}/research/${p.paper.slug}`,42      publishedTime: p.version.publishedAt.toISOString(),43    },44    other: {45      citation_title: p.version.title,46      citation_publication_date: p.version.publishedAt.toISOString().slice(0, 10),47      citation_technical_report_number: p.paper.publicId,48      citation_technical_report_institution: "AutoScholar.ai",49      citation_pdf_url: `${base}${p.version.pdfPublicPath}`,50    },51  };52}5354type ArtifactEntry = {55  path: string;56  kind: string;57  sha256: string;58  bytes: number;59  contentType: string;60  publicPath: string;61};62function Dl({ a }: { a: ArtifactEntry }) {63  return (64    <li className="flex items-center justify-between gap-3 py-1.5 text-sm">65      <a href={`${a.publicPath}?download=1`} className="font-mono text-xs break-all">66        {a.path}67      </a>68      <span className="subtle whitespace-nowrap text-xs">69        {fmtBytes(a.bytes)} · {shortHash(a.sha256, 8)}70      </span>71    </li>72  );73}7475export default async function PaperPage({ params }: { params: Promise<{ slug: string }> }) {76  const { slug } = await params;77  const p = await load(slug);78  if (!p) notFound();79  const base = process.env.APP_BASE_URL ?? "http://localhost:8470";80  const v = p.version;81  const meta = v.metadata as Record<string, unknown>;82  const withdrawn = meta.artifactsWithdrawn === true;83  const set = withdrawn ? [] : v.artifactSet;84  const url = `${base}/research/${p.paper.slug}`;85  const pub = new Date(v.publishedAt);86  const cite = {87    publicId: p.paper.publicId,88    title: v.title,89    year: pub.getUTCFullYear(),90    month: pub.getUTCMonth() + 1,91    versionNo: v.versionNo,92    url,93    keywords: v.keywords,94    abstract: v.abstract,95  };96  const figures = set.filter((a) => a.kind === "figure" && /\.(png|svg)$/i.test(a.path));97  const pdfFigures = set.filter((a) => a.kind === "figure" && /\.pdf$/i.test(a.path));98  const tex = set.filter((a) => a.kind === "manuscript" && /\.(tex|bib|cls|sty)$/i.test(a.path));99  const code = set.filter((a) => a.kind === "code");100  const data = set.filter((a) => a.kind === "data");101  const results = set.filter((a) => a.kind === "result" || a.kind === "table");102  const prov = (v.provenance ?? {}) as Record<string, unknown>;103  const qa = (v.qaSummary ?? {}) as {104    gates?: Array<{ gate: string; status: string; severity: string; message: string }>;105    warnings?: string[];106    policyVersion?: string;107    evaluatedAt?: string;108  };109  let bib: ReturnType<typeof parseBib> = [];110  const bibEntry = set.find((a) => a.path === "sources/references.bib");111  if (bibEntry) {112    try {113      bib = parseBib(114        (await storage().get(publicArtifactKey(p.paper.publicId, v.versionNo, bibEntry.path))).toString(115          "utf8",116        ),117      );118    } catch {119      bib = [];120    }121  }122  const jsonLd = {123    "@context": "https://schema.org",124    "@type": "ScholarlyArticle",125    headline: v.title,126    name: v.title,127    abstract: v.abstract,128    keywords: v.keywords.join(", "),129    datePublished: v.publishedAt.toISOString(),130    dateModified: p.paper.updatedAt.toISOString(),131    author: { "@type": "Organization", name: "AutoScholar (AI-generated research)", url: base },132    publisher: { "@type": "Organization", name: "AutoScholar.ai", url: base },133    identifier: p.paper.publicId,134    url,135    isAccessibleForFree: true,136    ...(set.length137      ? {138          encoding: {139            "@type": "MediaObject",140            contentUrl: `${base}${v.pdfPublicPath}`,141            encodingFormat: "application/pdf",142          },143        }144      : {}),145    about: disciplineLabel(p.paper.discipline),146    version: v.versionNo,147    ...(p.retraction ? { creativeWorkStatus: "Retracted" } : {}),148  };149  return (150    <article className="space-y-10">151      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD built from server data */}152      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />153      {p.retraction && (154        <div155          role="alert"156          className="rounded-md border p-4"157          style={{158            borderColor: "var(--color-danger-500)",159            background: "color-mix(in oklab, var(--color-danger-500) 8%, transparent)",160          }}161        >162          <p className="font-semibold">163            This working paper was retracted on {fmtDate(p.retraction.retractedAt)}.164          </p>165          <p className="mt-1 text-sm">{p.retraction.notice}</p>166          {withdrawn && (167            <p className="muted mt-1 text-sm">168              Its artifacts have been withdrawn from delivery. This page remains as a permanent record.169            </p>170          )}171        </div>172      )}173      <header className="grid gap-6 md:grid-cols-12">174        <div className="md:col-span-8">175          <div className="flex flex-wrap items-center gap-2 text-xs">176            <span className="font-mono">{p.paper.publicId}</span>177            <span className="subtle">·</span>178            <span>Version {v.versionNo}</span>179            <StatusBadge status={p.paper.status} />180            {p.paper.synthetic && <span className="badge">staging sample — not a real paper</span>}181          </div>182          <h1 className="serif mt-3 text-3xl font-semibold leading-tight sm:text-4xl">{v.title}</h1>183          <p className="mt-4 text-sm">184            <span className="font-medium">{t("byline")}</span>185            <span className="muted"> · Research computed on MacLustr</span>186          </p>187          <dl className="muted mt-4 grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-4">188            <div>189              <dt className="label">Released</dt>190              <dd>{fmtDate(p.paper.firstPublishedAt)}</dd>191            </div>192            <div>193              <dt className="label">Revised</dt>194              <dd>{fmtDate(v.publishedAt)}</dd>195            </div>196            <div>197              <dt className="label">Subject</dt>198              <dd>199                <Link href={`/research?discipline=${p.paper.discipline}`}>200                  {disciplineLabel(p.paper.discipline)}201                </Link>202              </dd>203            </div>204            <div>205              <dt className="label">Methodology</dt>206              <dd>{modeLabel(p.paper.researchMode)}</dd>207            </div>208          </dl>209          <ul className="mt-4 flex flex-wrap gap-2" aria-label="Keywords">210            {v.keywords.map((k) => (211              <li key={k}>212                <Link href={`/research?q=${encodeURIComponent(k)}`} className="badge no-underline">213                  {k}214                </Link>215              </li>216            ))}217          </ul>218        </div>219        <aside className="surface p-4 text-sm md:col-span-4">220          <p className="label mb-2">Downloads</p>221          {withdrawn ? (222            <p className="muted">Artifacts withdrawn.</p>223          ) : (224            <ul className="space-y-2">225              <li>226                <a className="btn btn-primary w-full" href={`${v.pdfPublicPath}?download=1`}>227                  PDF228                </a>229              </li>230              {tex.length > 0 && (231                <li>232                  <a233                    className="btn w-full"234                    href={`${set.find((a) => a.path === "manuscript/main.tex")?.publicPath ?? tex[0]!.publicPath}?download=1`}235                  >236                    LaTeX source237                  </a>238                </li>239              )}240              {code.length > 0 && (241                <li>242                  <a className="btn w-full" href="#code">243                    Code ({code.length} files)244                  </a>245                </li>246              )}247              {data.length > 0 && (248                <li>249                  <a className="btn w-full" href="#data">250                    Data ({data.length} files)251                  </a>252                </li>253              )}254            </ul>255          )}256          <div className="mt-4">257            <Disclosure compact />258          </div>259        </aside>260      </header>261262      <section aria-labelledby="abstract" className="grid gap-8 md:grid-cols-12">263        <div className="md:col-span-8">264          <h2 id="abstract" className="serif text-xl font-semibold">265            Abstract266          </h2>267          <p className="prose-paper mt-3">{v.abstract}</p>268          {Array.isArray(meta.contributions) && (meta.contributions as string[]).length > 0 && (269            <>270              <h2 className="serif mt-8 text-xl font-semibold">Key contributions</h2>271              <ol className="prose-paper mt-3 list-decimal pl-5">272                {(meta.contributions as string[]).map((c) => (273                  <li key={c}>{c}</li>274                ))}275              </ol>276            </>277          )}278          {typeof meta.methods === "string" && meta.methods && (279            <>280              <h2 className="serif mt-8 text-xl font-semibold">Methods</h2>281              <p className="prose-paper mt-3">{meta.methods}</p>282            </>283          )}284          {typeof meta.mainResults === "string" && meta.mainResults && (285            <>286              <h2 className="serif mt-8 text-xl font-semibold">Main results</h2>287              <p className="prose-paper mt-3">{meta.mainResults}</p>288            </>289          )}290          {typeof meta.limitations === "string" && meta.limitations && (291            <>292              <h2 className="serif mt-8 text-xl font-semibold">Limitations</h2>293              <p className="prose-paper mt-3">{meta.limitations}</p>294            </>295          )}296        </div>297        <div className="space-y-4 md:col-span-4">298          <div className="surface p-4 text-sm">299            <p className="label mb-2">Research status</p>300            <ul className="space-y-1">301              <li>AutoScholar working paper (preprint)</li>302              <li>Produced autonomously by an AI research workflow</li>303              <li>304                Automated critical review; <strong>not</strong> human peer review305              </li>306              <li>307                Deterministic quality gates:{" "}308                {qa.gates309                  ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} passed`310                  : "see provenance"}311              </li>312              <li>DOI: none (not registered)</li>313            </ul>314          </div>315          {p.summary.topics.length > 0 && (316            <div className="surface p-4 text-sm">317              <p className="label mb-2">Topics</p>318              <ul className="flex flex-wrap gap-2">319                {p.summary.topics.map((x) => (320                  <li key={x.slug}>321                    <Link href={`/topics/${x.slug}`} className="badge no-underline">322                      {x.name}323                    </Link>324                  </li>325                ))}326              </ul>327            </div>328          )}329        </div>330      </section>331332      {!withdrawn && (333        <section aria-labelledby="pdf">334          <h2 id="pdf" className="serif text-xl font-semibold">335            Read the paper336          </h2>337          <div className="mt-3">338            <PdfViewer339              src={v.pdfPublicPath}340              title={`${p.paper.publicId} v${v.versionNo} — ${v.title}`}341              downloadHref={`${v.pdfPublicPath}?download=1`}342            />343          </div>344        </section>345      )}346347      {(figures.length > 0 || pdfFigures.length > 0) && (348        <section aria-labelledby="figures">349          <h2 id="figures" className="serif text-xl font-semibold">350            Figures351          </h2>352          <ul className="mt-3 grid gap-4 sm:grid-cols-2">353            {figures.map((f) => (354              <li key={f.path} className="surface p-3">355                {/* biome-ignore lint/performance/noImgElement: artifact images are served immutable from this origin */}356                <img357                  src={f.publicPath}358                  alt={f.path.split("/").pop() ?? "figure"}359                  className="w-full"360                  loading="lazy"361                />362                <p className="subtle mt-2 font-mono text-xs">{f.path.replace("results/figures/", "")}</p>363              </li>364            ))}365            {pdfFigures.map((f) => (366              <li key={f.path} className="surface p-3 text-sm">367                <a href={`${f.publicPath}?download=1`}>{f.path.replace("results/figures/", "")}</a>{" "}368                <span className="subtle">(vector PDF, {fmtBytes(f.bytes)})</span>369              </li>370            ))}371          </ul>372        </section>373      )}374375      {bib.length > 0 && (376        <section aria-labelledby="refs">377          <h2 id="refs" className="serif text-xl font-semibold">378            References ({bib.length})379          </h2>380          <p className="muted mt-1 text-xs">381            Every reference was verified against a scholarly registry before publication; identifiers link to382            the source of record.383          </p>384          <ol className="mt-3 space-y-2 text-sm">385            {bib.map((b) => {386              const f = b.fields;387              const href = f.doi388                ? `https://doi.org/${f.doi.replace(/^https?:\/\/doi\.org\//, "")}`389                : (f.url ?? (f.eprint ? `https://arxiv.org/abs/${f.eprint}` : undefined));390              return (391                <li key={b.key} id={`ref-${b.key}`} className="border-b pb-2 hairline">392                  <span>{f.author ? `${f.author.replace(/\s+and\s+/g, ", ")}. ` : ""}</span>393                  {f.year && <span>({f.year}). </span>}394                  <span className="italic">{f.title?.replace(/[{}]/g, "")}</span>395                  {f.journal && <span>. {f.journal}</span>}396                  {f.booktitle && <span>. In {f.booktitle}</span>}397                  {f.volume && <span>, {f.volume}</span>}398                  {f.pages && <span>, {f.pages}</span>}.{" "}399                  {href && (400                    <a href={href} rel="noopener noreferrer" className="font-mono text-xs break-all">401                      {f.doi ? `doi:${f.doi}` : href}402                    </a>403                  )}404                </li>405              );406            })}407          </ol>408        </section>409      )}410411      {(code.length > 0 || data.length > 0 || results.length > 0) && (412        <section aria-labelledby="repro" className="grid gap-6 md:grid-cols-3">413          <h2 id="repro" className="serif text-xl font-semibold md:col-span-3">414            Code, data and results415          </h2>416          {code.length > 0 && (417            <div id="code" className="surface p-4">418              <p className="label mb-1">Code</p>419              <ul>420                {code.map((a) => (421                  <Dl key={a.path} a={a} />422                ))}423              </ul>424            </div>425          )}426          {data.length > 0 && (427            <div id="data" className="surface p-4">428              <p className="label mb-1">Data</p>429              <ul>430                {data.map((a) => (431                  <Dl key={a.path} a={a} />432                ))}433              </ul>434            </div>435          )}436          {results.length > 0 && (437            <div className="surface p-4">438              <p className="label mb-1">Results and tables</p>439              <ul>440                {results.map((a) => (441                  <Dl key={a.path} a={a} />442                ))}443              </ul>444            </div>445          )}446        </section>447      )}448449      <section aria-labelledby="reproducibility" className="grid gap-6 md:grid-cols-2">450        <div>451          <h2 id="reproducibility" className="serif text-xl font-semibold">452            Reproducibility453          </h2>454          <dl className="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm">455            <dt className="label">Data availability</dt>456            <dd>{(meta.dataAvailability as string) || "Not applicable or not stated."}</dd>457            <dt className="label">Code availability</dt>458            <dd>{(meta.codeAvailability as string) || "Not applicable or not stated."}</dd>459            <dt className="label">Ethics</dt>460            <dd>461              {(meta.ethics as string) ||462                "No restricted activities; public data, mathematics or simulations only."}463            </dd>464            <dt className="label">Toolchain</dt>465            <dd className="font-mono text-xs">466              {prov.toolchain && Object.keys(prov.toolchain as object).length467                ? Object.entries(prov.toolchain as Record<string, string>)468                    .map(([k, val]) => `${k} ${val}`)469                    .join(" · ")470                : "not recorded"}471            </dd>472            <dt className="label">LaTeX source</dt>473            <dd>474              {tex.length475                ? "Released; the PDF was rebuilt from it in a clean environment during verification."476                : "Not released."}477            </dd>478            <dt className="label">Verified</dt>479            <dd>480              {qa.evaluatedAt ? `${fmtDate(qa.evaluatedAt)} under quality policy ${qa.policyVersion}` : "—"}481            </dd>482          </dl>483          {qa.warnings && qa.warnings.length > 0 && (484            <details className="mt-3 text-sm">485              <summary className="cursor-pointer">486                Non-blocking warnings recorded at verification ({qa.warnings.length})487              </summary>488              <ul className="muted mt-2 list-disc pl-5">489                {qa.warnings.map((w) => (490                  <li key={w}>{w}</li>491                ))}492              </ul>493            </details>494          )}495        </div>496        <div>497          <h2 className="serif text-xl font-semibold">Provenance</h2>498          <dl className="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm">499            <dt className="label">Model</dt>500            <dd className="font-mono text-xs">{String(prov.model ?? "—")}</dd>501            <dt className="label">Reviewer model</dt>502            <dd className="font-mono text-xs">{String(prov.reviewerModel ?? "—")}</dd>503            <dt className="label">Instruction version</dt>504            <dd className="font-mono text-xs">505              v{String(prov.instructionVersion ?? "?")} · {shortHash(prov.instructionSha256 as string)}506            </dd>507            <dt className="label">Manifest digest</dt>508            <dd className="font-mono text-xs">{shortHash(prov.manifestSha256 as string)}</dd>509            <dt className="label">Execution</dt>510            <dd className="text-xs">511              {prov.startedAt512                ? `${fmtDate(prov.startedAt as string)} → ${prov.completedAt ? fmtDate(prov.completedAt as string) : "?"}`513                : "—"}{" "}514              · {String(prov.infrastructure ?? "MacLustr")}515            </dd>516            <dt className="label">Quality gates</dt>517            <dd>518              {qa.gates && qa.gates.length > 0 ? (519                <details className="text-xs">520                  <summary className="cursor-pointer">{qa.gates.length} gates evaluated</summary>521                  <ul className="mt-1 space-y-0.5">522                    {qa.gates.map((g) => (523                      <li key={g.gate}>524                        <StatusBadge status={g.status} /> <span className="font-mono">{g.gate}</span>{" "}525                        <span className="muted">— {g.message}</span>526                      </li>527                    ))}528                  </ul>529                </details>530              ) : (531                "—"532              )}533            </dd>534          </dl>535          <p className="subtle mt-2 text-xs">536            Artifact digests (SHA-256) are listed with each download; the full list is in537            provenance/checksums.sha256.538          </p>539        </div>540      </section>541542      <section aria-labelledby="cite">543        <h2 id="cite" className="serif text-xl font-semibold">544          Cite this working paper545        </h2>546        <div className="mt-3">547          <Tabs548            tabs={[549              {550                label: "BibTeX",551                content: (552                  <div>553                    <pre className="surface overflow-auto p-4 text-xs">{bibtexFor(cite)}</pre>554                    <div className="mt-2 flex gap-2">555                      <CopyButton text={bibtexFor(cite)} />556                      <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.bib`}>557                        Download .bib558                      </a>559                    </div>560                  </div>561                ),562              },563              {564                label: "RIS",565                content: (566                  <div>567                    <pre className="surface overflow-auto p-4 text-xs">{risFor(cite)}</pre>568                    <div className="mt-2 flex gap-2">569                      <CopyButton text={risFor(cite)} />570                      <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.ris`}>571                        Download .ris572                      </a>573                    </div>574                  </div>575                ),576              },577              {578                label: "Plain text",579                content: (580                  <div>581                    <p className="surface p-4 text-sm">{plainCitationFor(cite)}</p>582                    <div className="mt-2 flex gap-2">583                      <CopyButton text={plainCitationFor(cite)} />584                      <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.txt`}>585                        Download .txt586                      </a>587                    </div>588                  </div>589                ),590              },591            ]}592          />593        </div>594      </section>595596      <section aria-labelledby="history" className="grid gap-6 md:grid-cols-2">597        <div>598          <h2 id="history" className="serif text-xl font-semibold">599            Version history600          </h2>601          <ol className="mt-3 space-y-2 text-sm">602            {p.versions.map((x) => (603              <li key={x.id} className="surface flex items-start justify-between gap-3 p-3">604                <div>605                  <span className="font-medium">Version {x.versionNo}</span>{" "}606                  <span className="muted">· {fmtDate(x.publishedAt)}</span>607                  <p className="muted mt-1">{x.changelog}</p>608                </div>609                {!withdrawn && (610                  <a className="btn btn-sm" href={`${x.pdfPublicPath}?download=1`}>611                    PDF v{x.versionNo}612                  </a>613                )}614              </li>615            ))}616          </ol>617        </div>618        <div>619          <h2 className="serif text-xl font-semibold">Corrections and notices</h2>620          {p.corrections.length === 0 && !p.retraction ? (621            <p className="muted mt-3 text-sm">No corrections have been issued.</p>622          ) : (623            <ul className="mt-3 space-y-2 text-sm">624              {p.corrections.map((c) => (625                <li key={c.id} className="surface p-3">626                  <span className="font-medium">627                    Correction v{c.fromVersionNo} → v{c.toVersionNo}628                  </span>{" "}629                  <span className="muted">· {fmtDate(c.createdAt)}</span>630                  <p className="mt-1">{c.notice}</p>631                </li>632              ))}633              {p.retraction && (634                <li className="surface p-3">635                  <span className="font-medium">Retraction</span>{" "}636                  <span className="muted">· {fmtDate(p.retraction.retractedAt)}</span>637                  <p className="mt-1">{p.retraction.notice}</p>638                </li>639              )}640            </ul>641          )}642        </div>643      </section>644645      {p.related.length > 0 && (646        <section aria-labelledby="related">647          <h2 id="related" className="serif text-xl font-semibold">648            Related working papers649          </h2>650          <p className="muted mt-1 text-xs">651            Selected by shared discipline or keywords recorded in verified metadata.652          </p>653          <ul className="mt-3 grid gap-4 md:grid-cols-2">654            {p.related.map((r) => (655              <li key={r.publicId}>656                <PaperCard p={r} />657              </li>658            ))}659          </ul>660        </section>661      )}662      <Disclosure />663    </article>664  );665}666
Files · 87b4e55c3b9fbcc2cd191a1e469b33de0a24ccc8
Menu

Theme